/**
 * 文件工具类
 * 提供文件操作相关功能
 */
import * as fs from 'fs';
import * as path from 'path';
import { Response } from 'express';
import * as mime from 'mime-types';
import { getLogger } from '@/utils/logger';

// 创建file专用日志记录器
const logger = getLogger('file-util');

// 默认上传路径
const DEFAULT_UPLOAD_PATH = process.env.UPLOAD_PATH || 'uploads';

// 将异步函数转换为Promise
const fsStat = fs.promises.stat;
const fsReadFile = fs.promises.readFile;
const fsWriteFile = fs.promises.writeFile;
const fsUnlink = fs.promises.unlink;
const fsMkdir = fs.promises.mkdir;

export default class FileUtil {
  // 文件名正则表达式 (包含字母、数字、下划线、连字符、点和中文字符)
  public static readonly FILENAME_PATTERN = /^[a-zA-Z0-9_\-|.\u4e00-\u9fa5]+$/;

  // 默认允许下载的文件类型
  private static readonly DEFAULT_ALLOWED_EXTENSION = [
    // 图片
    "bmp", "gif", "jpg", "jpeg", "png",
    // 文档
    "doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt", "pdf",
    // 压缩文件
    "rar", "zip", "gz", "bz2",
    // 视频格式
    "mp4", "avi", "rmvb"
  ];

  /**
   * 输出指定文件的字节数组
   * 
   * @param filePath 文件路径
   * @param res Express响应对象
   */
  public static async sendFileBytes(filePath: string, res: Response): Promise<void> {
    try {
      await fs.promises.access(filePath, fs.constants.F_OK);
      
      const fileBuffer = await fsReadFile(filePath);
      
      // 获取文件类型
      const contentType = mime.contentType(path.extname(filePath)) || 'application/octet-stream';
      
      // 设置响应头
      res.setHeader('Content-Type', contentType);
      res.setHeader('Content-Disposition', `attachment; filename=${path.basename(filePath)}`);
      res.setHeader('Content-Length', fileBuffer.length);
      
      // 发送文件内容
      res.send(fileBuffer);
    } catch (error) {
      logger.error(`发送文件失败: ${filePath}`, { 
        error, 
        stack: error instanceof Error ? error.stack : undefined, 
        path: filePath 
      });
      throw new Error(`发送文件失败: ${error instanceof Error ? error.message : '未知错误'}`);
    }
  }

  /**
   * 写数据到文件中
   * 
   * @param data 数据
   * @param uploadDir 上传目录
   * @returns 目标文件的相对路径
   */
  public static async writeBytes(data: Buffer, uploadDir: string = DEFAULT_UPLOAD_PATH): Promise<string> {
    try {
      // 确保上传目录存在
      await this.ensureDir(uploadDir);
      
      // 获取文件扩展名
      const extension = this.getFileExtendName(data);
      
      // 生成日期路径和文件名
      const datePath = this.getDatePath();
      const fileName = `${this.generateUUID()}.${extension}`;
      const relativePath = path.join(datePath, fileName);
      const absolutePath = path.join(uploadDir, relativePath);
      
      // 确保目录存在
      await this.ensureDir(path.dirname(absolutePath));
      
      // 写入文件
      await fsWriteFile(absolutePath, data);
      
      logger.debug(`文件写入成功: ${absolutePath}`, { size: data.length, extension });
      return relativePath;
    } catch (error) {
      logger.error(`写入文件失败`, { 
        error, 
        stack: error instanceof Error ? error.stack : undefined, 
        uploadDir 
      });
      throw new Error(`写入文件失败: ${error instanceof Error ? error.message : '未知错误'}`);
    }
  }

  /**
   * 确保目录存在，如果不存在则创建
   * 
   * @param dir 目录路径
   */
  public static async ensureDir(dir: string): Promise<void> {
    try {
      try {
        await fs.promises.access(dir, fs.constants.F_OK);
      } catch (error) {
        await fsMkdir(dir, { recursive: true });
        logger.debug(`创建目录: ${dir}`);
      }
    } catch (error) {
      logger.error(`创建目录失败: ${dir}`, { 
        error, 
        stack: error instanceof Error ? error.stack : undefined 
      });
      throw new Error(`创建目录失败: ${error instanceof Error ? error.message : '未知错误'}`);
    }
  }

  /**
   * 删除文件
   * 
   * @param filePath 文件路径
   * @returns 是否删除成功
   */
  public static async deleteFile(filePath: string): Promise<boolean> {
    try {
      const exists = await this.exists(filePath);
      if (!exists) {
        logger.warn(`删除文件失败: 文件不存在 ${filePath}`);
        return false;
      }
      
      const stat = await fsStat(filePath);
      if (!stat.isFile()) {
        logger.warn(`删除文件失败: 路径不是文件 ${filePath}`);
        return false;
      }
      
      await fsUnlink(filePath);
      logger.debug(`删除文件成功: ${filePath}`);
      return true;
    } catch (error) {
      logger.error(`删除文件失败: ${filePath}`, { 
        error, 
        stack: error instanceof Error ? error.stack : undefined 
      });
      return false;
    }
  }

  /**
   * 文件名称验证
   * 
   * @param filename 文件名称
   * @returns true 正常 false 非法
   */
  public static isValidFilename(filename: string): boolean {
    return this.FILENAME_PATTERN.test(filename);
  }

  /**
   * 检查文件是否可下载
   * 
   * @param resource 需要下载的文件
   * @returns true 正常 false 非法
   */
  public static checkAllowDownload(resource: string): boolean {
    // 禁止目录上跳级别
    if (resource.includes('..')) {
      return false;
    }
    
    // 检查允许下载的文件规则
    const fileType = this.getFileType(resource);
    return this.DEFAULT_ALLOWED_EXTENSION.includes(fileType);
  }

  /**
   * 判断MIME类型是否是允许的MIME类型
   * 
   * @param extension 上传文件类型
   * @param allowedExtension 允许上传文件类型
   * @returns true/false
   */
  public static isAllowedExtension(extension: string, allowedExtension: string[]): boolean {
    return allowedExtension.some(ext => ext.toLowerCase() === extension.toLowerCase());
  }

  /**
   * 获取文件名的后缀（不含点）
   * 
   * @param fileName 文件名
   * @returns 文件后缀(不含点)
   */
  public static getFileType(fileName: string): string {
    return path.extname(fileName).substring(1).toLowerCase();
  }

  /**
   * 根据文件头判断文件扩展名
   * 
   * @param fileData 文件二进制数据
   * @returns 文件扩展名
   */
  public static getFileExtendName(fileData: Buffer): string {
    // 文件头标识与扩展名的映射
    const FILE_TYPE_MAP = {
      'ffd8ffe': 'jpg',
      '89504e47': 'png',
      '47494638': 'gif',
      '49492a00': 'tif',
      '424d': 'bmp',
      '504b0304': 'zip',
      '2f2aae5': 'js',
      '2f2ae585': 'css',
      '5b7bcd': 'json',
      '3c21444f': 'html',
      '255044': 'pdf',
      'd0cf11e0': 'doc',
      '504b34': 'docx'
    };
    
    // 读取前4个字节进行判断
    const firstBytes = fileData.slice(0, 4).toString('hex');
    
    for (const [header, extension] of Object.entries(FILE_TYPE_MAP)) {
      if (firstBytes.startsWith(header)) {
        return extension;
      }
    }
    
    // 默认返回二进制文件扩展名
    return 'bin';
  }

  /**
   * 获取文件名（不含路径）
   * 
   * @param filePath 文件路径
   * @returns 文件名
   */
  public static getFileName(filePath: string | null): string | null {
    if (filePath === null) {
      return null;
    }
    return path.basename(filePath);
  }

  /**
   * 获取文件名（不含后缀和路径）
   * 
   * @param filePath 文件路径
   * @returns 文件名（不含后缀）
   */
  public static getFileNameWithoutExtension(filePath: string | null): string | null {
    if (filePath === null) {
      return null;
    }
    
    const baseName = path.basename(filePath);
    const extName = path.extname(baseName);
    
    if (extName.length === 0) {
      return baseName;
    }
    
    return baseName.substring(0, baseName.length - extName.length);
  }

  /**
   * 获取文件的MIME类型
   * 
   * @param filePath 文件路径
   * @returns MIME类型
   */
  public static getMimeType(filePath: string): string {
    return mime.lookup(filePath) || 'application/octet-stream';
  }

  /**
   * 获取文件大小
   * 
   * @param filePath 文件路径
   * @returns 文件大小(字节)
   */
  public static async getFileSize(filePath: string): Promise<number> {
    try {
      const stat = await fsStat(filePath);
      return stat.size;
    } catch (error) {
      logger.error(`获取文件大小失败: ${filePath}`, { 
        error, 
        stack: error instanceof Error ? error.stack : undefined 
      });
      throw new Error(`获取文件大小失败: ${error instanceof Error ? error.message : '未知错误'}`);
    }
  }

  /**
   * 读取文件内容为字符串
   * 
   * @param filePath 文件路径
   * @param encoding 编码(默认utf8)
   * @returns 文件内容
   */
  public static async readAsString(filePath: string, encoding: BufferEncoding = 'utf8'): Promise<string> {
    try {
      return await fsReadFile(filePath, { encoding });
    } catch (error) {
      logger.error(`读取文件失败: ${filePath}`, { 
        error, 
        stack: error instanceof Error ? error.stack : undefined,
        encoding 
      });
      throw new Error(`读取文件失败: ${error instanceof Error ? error.message : '未知错误'}`);
    }
  }

  /**
   * 读取文件内容为Buffer
   * 
   * @param filePath 文件路径
   * @returns 文件内容Buffer
   */
  public static async readAsBuffer(filePath: string): Promise<Buffer> {
    try {
      return await fsReadFile(filePath);
    } catch (error) {
      logger.error(`读取文件失败: ${filePath}`, { 
        error, 
        stack: error instanceof Error ? error.stack : undefined 
      });
      throw new Error(`读取文件失败: ${error instanceof Error ? error.message : '未知错误'}`);
    }
  }

  /**
   * 判断文件或目录是否存在
   * 
   * @param path 路径
   * @returns 是否存在
   */
  public static async exists(path: string): Promise<boolean> {
    try {
      await fs.promises.access(path, fs.constants.F_OK);
      return true;
    } catch (error) {
      return false;
    }
  }

  /**
   * 判断路径是否是文件
   * 
   * @param path 路径
   * @returns 是否是文件
   */
  public static async isFile(path: string): Promise<boolean> {
    try {
      const stat = await fsStat(path);
      return stat.isFile();
    } catch (error) {
      return false;
    }
  }

  /**
   * 判断路径是否是目录
   * 
   * @param path 路径
   * @returns 是否是目录
   */
  public static async isDirectory(path: string): Promise<boolean> {
    try {
      const stat = await fsStat(path);
      return stat.isDirectory();
    } catch (error) {
      return false;
    }
  }

  /**
   * 递归删除目录
   * 
   * @param dir 目录路径
   * @returns 是否删除成功
   */
  public static async removeDirectory(dir: string): Promise<boolean> {
    try {
      await fs.promises.rm(dir, { recursive: true, force: true });
      logger.debug(`删除目录成功: ${dir}`);
      return true;
    } catch (error) {
      logger.error(`删除目录失败: ${dir}`, { 
        error, 
        stack: error instanceof Error ? error.stack : undefined 
      });
      return false;
    }
  }
  
  /**
   * 生成年/月/日格式的路径字符串
   * 
   * @returns 日期路径，如: 2022/08/08
   */
  private static getDatePath(): string {
    const now = new Date();
    const year = now.getFullYear();
    const month = (now.getMonth() + 1).toString().padStart(2, '0');
    const day = now.getDate().toString().padStart(2, '0');
    return `${year}/${month}/${day}`;
  }
  
  /**
   * 生成UUID
   * 
   * @returns UUID字符串
   */
  private static generateUUID(): string {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
      const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
      return v.toString(16);
    });
  }
} 